- Published on
섹션 6: Laser Defender 3
#Laser Defender - 3
Unity 2D 슈팅 게임의 완성도를 높이는 마지막 가이드입니다. 오디오 시스템, UI 설계, 점수 시스템 등을 구현합니다.
#📋 목차
#{ Unity 게임에 오디오 추가 }
이 강의에서는 Unity 게임에 오디오 효과를 추가하는 과정을 다룹니다. 발사음과 데미지 효과음을 구현
#1. 오디오 클립 준비
작업: 사용할 오디오 클립을 다운로드하고 프로젝트에 추가.
세부 과정:
1. 오디오 소스를 찾기 위해 kenney.nl 같은 사이트에서 'sci-fi' 오디오 팩을 다운로드.
2. 다운로드한 오디오 파일을 Unity 프로젝트의 [Assets pack] 폴더 내 sci-fi sounds 폴더에 추가.
개념:
오디오 클립: 짧은 소리(예: 발사음, 폭발음)를 의미하며, Unity에서는 AudioClip 타입으로 관리.
Unity에서 오디오를 재생하려면 AudioSource 컴포넌트나 AudioSource.PlayClipAtPoint 같은 메서드를 사용.
짧은 오디오 클립은 런타임에 재생 후 자동으로 사라지도록 설정 가능.
#2. AudioPlayer 게임 오브젝트 및 스크립트 생성
작업: 오디오를 관리할 AudioPlayer 오브젝트와 스크립트를 생성.
세부 과정:
1. Hierarchy에서 우클릭 → Create Empty로 새 게임 오브젝트 생성 → 이름: AudioPlayer.
2. Transform 리셋으로 위치 초기화.
3. [Scripts] 폴더에서 새 C# 스크립트 생성 → 이름: AudioPlayer.
4. AudioPlayer 오브젝트에 스크립트를 추가.
개념:
게임 오브젝트: Unity에서 오디오를 재생하려면 오브젝트에 스크립트를 붙여 관리.
Transform 리셋: 오브젝트의 위치, 회전, 크기를 기본값(0, 0, 0)으로 설정해 깔끔하게 관리.
#3. AudioPlayer 스크립트 작성
작업: 발사음과 데미지음을 재생할 수 있도록 AudioPlayer 스크립트 작성.
#1. 변수 선언:
[Header("Shooting")]
[SerializeField] private AudioClip shootClip;
[SerializeField][Range(0f, 1f)] float shootVolume = 1f;
[Header("Damage")]
[SerializeField] private AudioClip damageClip;
[SerializeField][Range(0f, 1f)] float damageVolume = 1f;
[Header]: Inspector에서 변수를 그룹화해 가독성 향상.
[SerializeField]: private 변수를 Inspector에서 수정 가능하도록 설정.
[Range(0f, 1f)]: 볼륨을 0~1 사이로 제한하며 슬라이더 제공.
#2. 공통 재생 메서드
private void PlayClip(AudioClip clip, float volume)
{
if (clip != null)
{
Vector3 cameraPos = Camera.main.transform.position;
AudioSource.PlayClipAtPoint(clip, cameraPos, volume);
}
}
AudioSource.PlayClipAtPoint: 오디오 클립을 특정 위치(카메라 위치)에서 재생 후 자동 소멸.
#3. 발사음 재생 메서드
public void PlayShootingClip()
{
if (shootClip != null)
{
PlayClip(shootClip, shootVolume);
}
}
#4. 데미지음 재생 메서드
public void PlayDamageClip()
{
if (damageClip != null)
{
PlayClip(damageClip, damageVolume);
}
}
AudioSource.PlayClipAtPoint: 짧은 오디오 클립을 재생할 때 유용. 클립, 위치, 볼륨을 인자로 받음.
코드 재사용: PlayClip 메서드를 만들어 중복 코드 줄이고 유지보수성 향상.
#4. Shooter 스크립트 수정 (발사음 추가)
작업: Shooter 스크립트에서 발사 시 오디오 재생.
#1. AudioPlayer 참조 추가
AudioPlayer audioPlayer;
void Awake()
{
audioPlayer = FindAnyObjectByType<AudioPlayer>();
}
FindAnyObjectByType: 씬에서 AudioPlayer 컴포넌트를 찾아 참조.
#2. FireContinuously 코루틴에서 발사음 재생
IEnumerator FireContinuously()
{
while (true)
{
GameObject instance = Instantiate(projectilePrefab, transform.position, Quaternion.identity);
Rigidbody2D rb = instance.GetComponent<Rigidbody2D>();
if (rb != null)
{
rb.velocity = transform.up * projectileSpeed;
}
Destroy(instance, projectileLifetime);
float timeToNextProjectile = Random.Range(baseFiringRate - firingRateVariance, baseFiringRate + firingRateVariance);
timeToNextProjectile = Mathf.Clamp(timeToNextProjectile, minimumFiringRate, float.MaxValue);
audioPlayer.PlayShootingClip(); // 발사음 재생
yield return new WaitForSeconds(timeToNextProjectile);
}
}
발사체 생성 후 호출.
코루틴: FireContinuously는 반복적인 발사를 관리. yield return으로 시간 간격 조정.
FindAnyObjectByType: 단일 인스턴스 스크립트를 찾을 때 유용하지만, 여러 개일 경우 주의.
#5. Health 스크립트 수정 (데미지음 추가)
작업: Health 스크립트에서 데미지 발생 시 오디오 재생.
#1. AudioPlayer 참조 추가:
AudioPlayer audioPlayer;
void Awake()
{
cameraShake = Camera.main.GetComponent<CameraShake>();
audioPlayer = FindAnyObjectByType<AudioPlayer>();
}
#2. OnTriggerEnter2D에서 데미지음 재생
void OnTriggerEnter2D(Collider2D collision)
{
DamageDealer damageDealer = collision.gameObject.GetComponent<DamageDealer>();
if (damageDealer != null)
{
TakeDamage(damageDealer.GetDamage());
PlayHitEffect();
audioPlayer.PlayDamageClip(); // 데미지 처리 후 호출.
ShakeCamera();
damageDealer.Hit();
}
}
데미지 처리 후 호출.
OnTriggerEnter2D: 2D 충돌 시 호출되는 Unity 이벤트.
데미지음은 데미지 발생 시점(TakeDamage 호출 후)에 재생.
#6. Unity에서 설정 및 테스트
작업: Inspector에서 오디오 클립 설정 및 테스트.
세부 과정:
1. AudioPlayer 오브젝트의 Inspector에서:
Shoot Clip: 예: laserSmall_001.
Damage Clip: 예: ExplosionCrunch_04.
볼륨 슬라이더로 조정(기본값: 1f).
Game 창에서 Mute Audio가 체크 해제되었는지 확인.
2. 플레이 테스트:
발사 시 laserSmall_001 소리 확인.
데미지 시 ExplosionCrunch_04 소리 확인.
4. 발사음이 너무 빠르면 Shooter 스크립트의 baseFiringRate를 조정(예: 0.4초).
개념:
Inspector 설정: [SerializeField]로 선언한 변수는 Unity Inspector에서 설정 가능.
Mute Audio: Game 창의 오디오 뮤트 설정을 확인하지 않으면 소리가 안 들릴 수 있음.
#7. 핵심 개념 요약
오디오 클립: 짧은 효과음은 AudioClip으로 관리, AudioSource.PlayClipAtPoint로 재생.
AudioPlayer: 오디오 재생을 중앙화해 관리, 코드 재사용성 향상.
Inspector 활용: [SerializeField]와 [Range]로 설정 편리성 제공.
스크립트 간 참조: FindAnyObjectByType으로 다른 스크립트 접근.
테스트 주의점: Game 창의 Mute Audio 확인, 발사 속도 조정으로 소리 겹침 방지.
#{ 게임에 배경 음악 추가하기 }
#1. 배경 음악 찾기
사이트: Opengameart.org에서 인디 개발자를 위한 무료 리소스를 찾음.
개념: CC0 라이선스는 저작권 제한 없이 자유롭게 사용할 수 있는 퍼블릭 도메인 음악을 의미. 게임 개발 시 저작권 문제 없이 사용 가능.
#2. 유니티에서 오디오 시스템 이해
유니티 오디오의 3요소:
1. Audio Listener: 소리를 듣는 객체. 기본적으로 메인 카메라에 붙어 있음.
예: Main Camera에 Audio Listener 컴포넌트가 있어 소리가 들림.
2. Audio Source: 오디오를 재생하는 객체. 오디오 클립을 재생하도록 설정.
예: AudioSource.PlayClipAtPoint는 일회성 소리 재생에 사용.
3. Audio File: 실제 재생될 오디오 파일(예: MP3, WAV).
흐름: Audio File → Audio Source → Audio Listener.
개념:
Audio Listener는 게임 내에서 플레이어가 소리를 듣는 기준점.
Audio Source는 소리의 위치, 볼륨, 루핑 여부 등을 제어.
One-shot Audio는 짧게 한 번 재생되는 소리(예: 총소리), 배경 음악은 루핑으로 계속 재생.
#3. 배경 음악 추가하기
Audio Player 설정:
1. Audio Player 게임 오브젝트에 새 컴포넌트로 Audio Source 추가.
2. 설정:
AudioClip: 원하는 음악 파일 드래그하여 할당.
Play on Awake: 게임 시작 시 자동 재생(체크).
Loop: 음악이 끝나면 반복 재생(체크).
Volume: 볼륨 조절(0~1 슬라이더).
Spatial Blend: 2D로 설정(3D는 소리 위치에 따라 볼륨 변화, 2D는 균일).
3D Sound Settings, Output, Bypass Effects 등은 2D 게임에서 무시.
3. 결과: 게임 시작 시 배경 음악이 루핑되며 재생.
개념:
Audio Source 컴포넌트는 오디오 재생의 핵심. Play on Awake와 Loop 옵션은 배경 음악에 필수.
2D vs 3D 사운드: 2D는 위치 상관없이 균일한 소리, 3D는 거리/방향에 따라 소리 크기 변화.
#{ 점수 및 에너지 시스템 구현 }
#목표
1. Health 스크립트에 플레이어의 현재 체력(에너지)을 반환하는 public 게터 메서드를 추가.
2. ScoreKeeper 스크립트를 작성해 점수를 관리:
private 점수 변수.
점수를 반환하는 public 게터 메서드.
점수를 수정하는 public 메서드.
점수를 초기화하는 public 메서드.
3. 적이 파괴될 때 점수가 증가하도록 Health 스크립트 수정.
4. Unity에서 이를 테스트하고 UI로 점수/체력을 표시할 준비.
#1. Health 스크립트 수정
Health.cs는 플레이어와 적의 체력을 관리하며, 적이 죽을 때 점수를 추가하는 역할을 합니다.
#1.1. public 게터 메서드 추가
목표: 플레이어의 현재 체력을 반환하는 메서드 추가.
작업:
Health 스크립트에 public int GetHealth() 메서드를 추가.
이 메서드는 private int health 값을 반환.
public int GetHealth()
{
return health;
}
개념: 게터 메서드는 private 변수의 값을 외부에서 안전하게 읽을 수 있도록 해줍니다. UI에서 체력을 표시하려면 이 메서드가 필요합니다.
#1.2. 플레이어와 적 구분
문제: 플레이어와 적이 같은 Health 스크립트를 사용하므로, 플레이어가 죽을 때 점수가 추가되지 않도록 해야 함.
작업:
[SerializeField] bool isPlayer 변수를 추가해 플레이어인지 적인지 구분.
Unity Inspector에서 플레이어 오브젝트는 isPlayer를 체크, 적은 체크 해제.
[SerializeField] bool isPlayer = false;
개념: [SerializeField]는 private 변수를 Unity Inspector에서 수정 가능하게 만듭니다.
이를 통해 동일한 스크립트를 플레이어와 적에 다르게 설정할 수 있습니다.
#1.3. 점수 변수 추가
목표: 적이 죽을 때 추가할 점수를 설정.
작업:
[SerializeField] int score = 50; 변수를 추가.
이 값은 적이 파괴될 때 ScoreKeeper에 추가될 점수.
[SerializeField] int score = 50;
개념: 적마다 다른 점수를 설정할 수 있어 게임 디자인의 유연성을 높입니다. 예: 강한 적은 높은 점수, 약한 적은 낮은 점수.
#1.4. 점수 추가 로직
목표: 적이 죽을 때 점수를 추가.
작업:
ScoreKeeper를 참조하도록 Health 스크립트에 변수 추가.
Awake에서 FindAnyObjectByType<ScoreKeeper>()로 ScoreKeeper를 찾음.
Die 메서드에서 isPlayer가 false인 경우(즉, 적인 경우) ScoreKeeper.ModifyScore(score) 호출.
ScoreKeeper scoreKeeper;
void Awake()
{
scoreKeeper = FindAnyObjectByType<ScoreKeeper>();
// ... 기존 코드 ...
}
void Die()
{
if (!isPlayer)
{
scoreKeeper.ModifyScore(score);
}
Destroy(gameObject);
}
개념: FindAnyObjectByType는 씬에서 특정 컴포넌트를 찾아 참조합니다.
Die 메서드는 객체가 파괴되기 전에 점수를 추가해 게임 로직을 유지합니다.
#2. ScoreKeeper 스크립트 작성
ScoreKeeper.cs는 점수를 관리하는 스크립트로, 점수를 저장하고 수정하며 UI로 내보낼 준비를 합니다.
#2.1. 점수 변수와 게터 메서드
작업:
private int score = 0; 변수로 점수를 저장.
public int GetScore() 메서드로 점수 반환.
private int score = 0;
public int GetScore()
{
return score;
}
개념: private 변수는 외부에서 직접 수정되지 않도록 보호하며,
public 게터 메서드는 UI나 다른 스크립트에서 점수를 읽을 수 있게 합니다.
#2.2. 점수 수정 메서드
작업:
public void ModifyScore(int value) 메서드로 점수 추가.
Mathf.Clamp로 점수가 0 아래로 내려가지 않도록 제한.
디버깅을 위해 Debug.Log 추가.
public void ModifyScore(int value)
{
score += value;
score = Mathf.Clamp(score, 0, int.MaxValue);
Debug.Log("Score updated: " + score);
}
개념: Mathf.Clamp는 값을 최소값(0)과 최대값(int.MaxValue) 사이로 제한합니다.
Debug.Log는 콘솔에 로그를 출력해 디버깅에 유용합니다.
#2.3. 점수 초기화 메서드
작업:
public void ResetScore() 메서드로 점수를 0으로 초기화.
public void ResetScore()
{
score = 0;
}
개념: 초기화 메서드는 게임 재시작 시 점수를 리셋하는 데 사용됩니다.
#3. Unity 설정
작업:
1. ScoreKeeper 오브젝트 생성:
Hierarchy에서 빈 게임 오브젝트 생성 후 이름 ScoreKeeper로 설정.
ScoreKeeper 스크립트를 추가.
2. 플레이어 설정:
플레이어 오브젝트의 Health 컴포넌트에서 Is Player 체크.
Score 값을 0으로 설정(플레이어는 점수를 주지 않음).
3. 적 설정:
적 오브젝트의 Health 컴포넌트에서 Is Player 체크 해제.
Score 값을 50(또는 원하는 값)으로 설정.
4. 테스트:
게임 실행 후 적을 파괴해 콘솔에서 Debug.Log로 점수 증가 확인.
개념: Unity의 Inspector를 통해 스크립트의 [SerializeField] 변수를 설정하면,
동일한 스크립트를 여러 오브젝트에 다르게 적용할 수 있습니다.
#요약
1. Health 스크립트:
GetHealth()로 체력 반환.
isPlayer로 플레이어/적 구분.
score 변수로 적 파괴 시 점수 설정.
Die 메서드에서 적 파괴 시 점수 추가.
2. ScoreKeeper 스크립트:
score 저장, GetScore로 반환.
ModifyScore로 점수 수정(0 이하 방지).
ResetScore로 점수 초기화.
3. Unity 설정:
ScoreKeeper 오브젝트 생성.
플레이어와 적의 Health 컴포넌트 설정.
콘솔로 점수 증가 테스트.
#{ UI 설계 및 구현 }
#1. UI 설계의 개요
목표:
게임의 핵심 정보(플레이어 에너지, 점수 등)를 플레이어에게 효과적으로 전달하는 UI를 설계하고 구현.
UI의 역할:
게임 정보를 직관적으로 보여주되, 게임 플레이를 방해하지 않도록 적절히 배치.
고려사항:
플레이어 시선: 플레이어는 주로 화면 중앙(적군, 플레이어 비행선)에 집중.
UI 배치: 시선을 방해하지 않도록 화면 하단에 UI 배치 권장.
확장 가능성: 기본적으로 에너지(Health)와 점수(Score)를 표시하지만, 필요 시 추가 요소(예: 웨이브 정보, 에너지 보충제 상태, 플레이 시간 등) 고려.
개념 설명:
UI는 직관성과 비침투성(플레이를 방해하지 않음)을 균형 있게 설계해야 합니다.
플레이어가 자주 보는 화면 영역을 분석해 UI를 방해가 적은 위치에 배치하는 것이 중요합니다.
#2. UI 요소 선택
게임 UI에 포함할 요소를 결정합니다. 강의에서는 다음과 같은 요소를 선택:
에너지(Health): 플레이어의 현재 에너지 상태를 표시.
옵션:
슬라이더(간단하고 직관적)
하트 컨테이너(시각적 표현)
퍼센트 텍스트(간단한 숫자 표시)
스프라이트 주변 아이콘/데미지 효과(직관적이지만 UI와 직접 관련 없음)
결정: 슬라이더 사용(단순하고 연습에 적합).
점수(Score): 게임 진행 상황을 나타내는 점수 표시.
옵션: 텍스트로 표시(점수는 숫자로 표현하는 것이 일반적).
확장 가능 요소 (선택 사항):
현재/다음 웨이브 정보.
에너지 보충제 상태.
플레이 시간(무한 진행 게임의 경우).
개념 설명:
UI 요소는 게임의 핵심 정보를 반영해야 하며,
플레이어가 한눈에 이해할 수 있도록 단순하고 명확한 형태를 선택하는 것이 좋습니다.
슬라이더는 연속적인 값을 시각적으로 보여주기에 적합하고, 텍스트는 숫자 정보를 명확히 전달합니다.
#3. 유니티에서 UI 구현
#3.1. 캔버스 추가
작업:
Hierarchy에서 우측 클릭 → UI → Canvas 추가.
캔버스 추가 시 EventSystem 자동 생성.
경고 해결: EventSystem에서 경고(구형 입력 시스템 사용) 발생 시, Replace 버튼 클릭해 최신 입력 시스템으로 전환.
설명:
Canvas: 모든 UI 요소의 부모 객체. UI의 배치와 스케일링을 관리.
역할: 화면 크기에 따라 UI를 렌더링하고, UI 요소의 계층 구조를 정의.
특징: 캔버스는 2D 공간에서 UI를 배치하며, 3D 게임 월드와 별개로 동작.
EventSystem: UI 상호작용(버튼 클릭, 슬라이더 드래그 등)을 처리.
구형 vs 최신 입력 시스템: 유니티의 최신 입력 시스템(Input System 패키지)은 더 유연하고 다양한 입력 장치를 지원.
Replace는 구형 시스템(Input Manager)을 최신 시스템으로 교체.
개념 설명:
Canvas의 중요성: 캔버스는 UI의 "캔버스"로, 모든 UI 요소를 포함하는 컨테이너. 반응형 UI를 위해 캔버스 설정이 핵심.
EventSystem의 역할: UI와 플레이어 입력 간의 연결을 담당. 최신 입력 시스템은 멀티플랫폼 지원과 커스터마이징이 용이.
#3.2. 캔버스 설정 (Canvas Scaler)
작업:
Canvas 선택 → Canvas Scaler 컴포넌트 수정.
UI Scale Mode:
기본값: Constant Pixel Size (고정 픽셀 크기).
변경: Scale With Screen Size (화면 크기에 비례).
Reference Resolution: 1080x1920 (9:16 비율, HD 화질) 설정.
설명:
Constant Pixel Size: UI 요소의 픽셀 크기가 고정. 다양한 해상도에서 UI 크기가 일정하지 않을 수 있음.
단점: 작은 화면에서는 UI가 너무 크게 보이고, 큰 화면에서는 작게 보임.
Scale With Screen Size: UI 요소가 화면 해상도에 비례해 크기 조정.
장점: 모든 화면 크기에서 UI 비율이 일정. 반응형 디자인에 적합.
Reference Resolution: UI 설계의 기준 해상도. 1080x1920은 모바일 게임에 적합한 표준 HD 해상도.
결과: UI 요소가 화면 크기에 따라 동적으로 조정되어 일관된 비율 유지.
개념 설명:
반응형 UI: 다양한 디바이스(모바일, PC, 태블릿)에서 UI가 적절히 보이도록 스케일링하는 것.
Scale With Screen Size는 이를 구현하는 표준 방법.
Reference Resolution: UI 레이아웃을 설계할 때 기준이 되는 해상도.
실제 화면 해상도와 달라도 비율을 유지하며 스케일링.
#3.3. UI 패널 추가
작업:
캔버스 내에 UI → Panel 추가.
크기 조정: 화면 하단 7% 차지하도록 설정.
Anchor Presets로 앵커를 하단으로 설정.
RectTransform에서 Top을 0으로 설정해 하단 정렬.
Game View에서 리사이즈 테스트해 비율 유지 확인.
설명:
Panel: UI 요소를 그룹화하는 컨테이너. 배경 이미지 역할도 수행.
용도: 슬라이더, 텍스트 등 여러 UI 요소를 한 곳에 정리.
시각적 역할: 색상이나 투명도로 UI의 시각적 계층을 구분.
Anchor: UI 요소의 상대적 위치를 고정하는 기준점.
역할: 화면 크기가 변해도 UI 요소가 지정된 위치(예: 하단)에 유지되도록 함.
설정 방법: Anchor Presets로 미리 정의된 위치 선택(예: 하단 중앙)하거나, 수동으로 앵커 좌표 입력.
RectTransform: UI 요소의 위치, 크기, 회전을 정의하는 컴포넌트.
Top=0: 패널이 화면 하단에 붙도록 설정.
비율 유지: 화면 크기 변화에도 패널이 7% 비율을 유지.
개념 설명:
패널의 역할: UI 요소를 그룹화해 관리 편의성과 시각적 일관성을 제공. 예: 배경 색상으로 UI 영역을 구분.
앵커의 중요성: 반응형 UI의 핵심. 앵커는 UI 요소가 화면의 특정 위치(상단, 하단, 중앙 등)에
상대적으로 고정되도록 함. 예: 하단 앵커는 UI를 항상 화면 하단에 유지.
RectTransform: 일반 3D 객체의 Transform과 달리, 2D UI를 위한 위치/크기 제어 도구. 앵커와 함께 사용해 반응형 배치 구현.
#3.4. 에너지 슬라이더 추가 (HealthSlider)
작업:
패널 내에 UI → Slider 추가, 이름 HealthSlider.
앵커 설정: 패널 높이에 맞게, 좌우 패딩(Left: 25, Right: 25) 추가.
설정:
Interactable 끄기: 플레이어가 슬라이더를 조작하지 못하도록.
Transition을 None으로: 슬라이더 이동 시 애니메이션 효과 제거.
Value를 0.5로: 테스트용 초기 값 설정.
Handle 비활성화/삭제: 플레이어가 조작하지 않으므로 불필요.
설명:
Slider: 연속적인 값을 시각적으로 표현(예: 0~100% 에너지).
구성 요소:
Background: 슬라이더의 배경 바.
Fill Area: 현재 값을 채우는 색상 바.
Handle: 슬라이더를 드래그하는 핸들(비활성화 가능).
Interactable: 슬라이더의 상호작용 여부. 끄면 플레이어가 슬라이더를 움직일 수 없음.
Transition: 슬라이더 값 변경 시 애니메이션 효과. None으로 설정하면 즉시 반영.
패딩: 슬라이더가 패널 경계에 너무 붙지 않도록 여백 추가.
개념 설명:
슬라이더의 구조: 슬라이더는 Background, Fill Area, Handle로 구성. Fill Area는 값에 따라 동적으로 채워짐.
비상호작용 UI: 게임 상태 표시용 UI는 플레이어 조작을 막아야 함. Interactable을 끄면 슬라이더는 표시 전용이 됨.
패딩의 역할: UI 요소 간 여백은 가독성과 시각적 깔끔함을 높임.
#3.5. 점수 텍스트 추가
작업:
패널 내에 UI → Text - TextMeshPro 추가.
TMP Essentials 임포트: TextMeshPro 사용 전 필수 리소스 임포트 (Window → TextMeshPro → Import TMP Essential Resources).
앵커 설정: 패널 중앙, 좌우 패딩(Left: 25, Right: 25), Top: 0, Bottom: 0.
텍스트 설정:
Auto Size 활성화: 텍스트 크기가 화면 크기에 따라 조정.
폰트 크기: 최소 5, 최대 72.
정렬: 오른쪽, 가운데.
테스트용 숫자 입력(예: "1234").
설명:
TextMeshPro: 유니티의 기본 텍스트보다 고급 렌더링과 스타일링 제공.
장점: 선명한 텍스트, 아웃라인, 그림자, 글로우 등 다양한 효과 지원.
TMP Essentials: TextMeshPro 사용에 필요한 기본 폰트와 리소스.
Auto Size: 텍스트 크기를 화면 해상도에 맞게 자동 조정.
최소/최대 폰트 크기: 텍스트가 너무 작거나 커지지 않도록 제한.
정렬: 텍스트의 수평/수직 위치를 조정해 가독성 향상.
개념 설명:
TextMeshPro의 장점: 고해상도 텍스트 렌더링과 다양한 스타일링 옵션(아웃라인, 그림자 등)으로 UI의 시각적 품질 향상.
Auto Size: 반응형 UI에서 텍스트 크기를 동적으로 조정해 모든 화면에서 가독성 유지.
패딩과 정렬: UI 요소의 깔끔한 배치를 위해 여백과 정렬은 필수.
#**3.6. UI 디자인 개선**
작업:
패널:
Image 컴포넌트의 Color 변경(예: 파란색/보라색 톤, 불투명도 조정).
HealthSlider:
Background: 보라색, 불투명도 100%.
Fill Area: 파란색.
TextMeshPro:
폰트 변경: KenneySpaceFont 사용.
Font Asset 생성: Window → TextMeshPro → Font Asset Creator → 폰트 선택 → Generate Font Atlas.
적용: 생성된 Font Asset을 TextMeshPro에 드래그.
색상: 슬라이더와 조화로운 파란색/보라색, 불투명도 조정.
아웃라인: 두께 0.25, 불투명도 100.
글로우 효과: 텍스트를 돋보이게 함.
설명:
색상: UI의 색상은 게임 분위기와 일관성을 유지하며 가독성을 높임.
예: 파란색/보라색은 미래적/우주적 테마에 적합.
Font Asset: TextMeshPro에서 커스텀 폰트를 사용하려면 폰트를 Font Asset으로 변환해야 함.
Font Atlas: 폰트의 텍스처를 생성해 렌더링 최적화.
아웃라인/글로우: 텍스트를 배경과 구분해 가독성을 높이고 시각적 매력을 추가.
개념 설명:
색상 심리학: 색상은 게임의 분위기를 강화. 예: 파란색은 차분하고 신뢰감, 보라색은 신비로움 전달.
Font Asset의 필요성: TextMeshPro는 커스텀 폰트를 고품질로 렌더링하기 위해 Font Atlas를 생성. 이는 텍스트의 선명도와 효과 적용을 보장.
시각적 효과: 아웃라인과 글로우는 텍스트를 배경에서 돋보이게 하여 가독성과 미적 품질을 동시에 충족.
패널 추가
엥커 조절
슬라이드 바 앵커를 조절 ( 앵커는 x 표시로 4개가 분할된 형태 )
텍스트 앵커 설정과 25 정도의 패팅 그리고 글자 크기 auto
폰트 추가
폰트 에셋 만들기
#{ UI 연결 및 구현 }
이 강의에서는 Unity에서 게임 UI를 구현하고, 이를 게임 로직과 연결하는 과정을 다룹니다.
UIDisplay 스크립트를 만들어 캔버스에 첨부하고,
플레이어의 체력(Health)과 점수(Score)를 UI에 실시간으로 반영하는 방법을 배웁니다.
#1. 목표
목적:
UIDisplay 스크립트를 작성하여 UI 요소(체력 슬라이더, 점수 텍스트)를 게임 로직(Health, ScoreKeeper)과 연결해 실시간으로 업데이트.
결과:
플레이어의 체력과 점수가 UI에 반영되고, 점수 형식을 아케이드 스타일로 포맷팅.
#2. 사전 준비
필요한 컴포넌트:
Health: 플레이어의 체력 정보를 관리하는 스크립트.
ScoreKeeper: 게임 점수를 관리하는 스크립트.
Canvas: UI 요소(체력 슬라이더, 점수 텍스트)를 포함하는 Unity의 UI 컨테이너.
UI 요소:
Slider: 플레이어의 체력을 시각적으로 표시.
TextMeshProUGUI: 점수를 텍스트로 표시.
#3. 구현 단계
#3.1. UIDisplay 스크립트 생성
1. 스크립트 생성:
Unity 프로젝트의 [Scripts] 폴더에서 새 C# 스크립트를 생성하고, 이름을 UIDisplay로 지정.
스크립트를 캔버스 오브젝트에 추가(드래그 앤 드롭).
2. 필요한 네임스페이스 추가:
UI와 TextMeshPro를 사용하기 위해 아래 두 줄을 스크립트 상단에 추가:
using UnityEngine.UI; // Slider 등 UI 컴포넌트 사용
using TMPro; // TextMeshPro 텍스트 사용
개념: 네임스페이스는 관련 기능(클래스, 메소드 등)을 묶는 코드 그룹입니다.
UnityEngine.UI는 Unity의 기본 UI 시스템, TMPro는 TextMeshPro를 다루기 위해 필요합니다.
#3.2. 변수 선언
변수는 UI와 게임 데이터를 연결하기 위해 사용됩니다. [SerializeField]를 사용해 Unity Inspector에서 수동으로 연결 가능.
두 개의 헤더로 구분:
[Header("Health")]
[SerializeField] Slider healthSlider; // 체력 슬라이더
[SerializeField] Health playerHealth; // 플레이어 체력 스크립트
[Header("Score")]
[SerializeField] TextMeshProUGUI scoreText; // 점수 텍스트
ScoreKeeper scoreKeeper; // 점수 관리 스크립트
개념:
[SerializeField]: private 변수라도 Unity Inspector에서 수정 가능하도록 설정.
Slider: Unity의 UI 컴포넌트로, 체력 바 같은 비율을 시각적으로 표시.
TextMeshProUGUI: 고급 텍스트 렌더링 컴포넌트로, 점수 표시용.
ScoreKeeper는 Scene에 단 하나만 존재하므로, FindObjectOfType으로 동적으로 가져옴.
#3.3. Awake에서 ScoreKeeper 연결
ScoreKeeper는 수동 연결 대신 코드로 찾아옴:
void Awake()
{
scoreKeeper = FindObjectOfType<ScoreKeeper>();
}
개념: Start는 오브젝트 초기화 직후 호출. 여기서는 슬라이더의 maxValue를 플레이어의 초기 체력으로 설정해
UI가 게임 데이터와 동기화되도록 함. 이렇게 하면 체력 값이 바뀌어도 UI가 자동으로 반영.
#3.5. Update에서 UI 업데이트
매 프레임 UI를 갱신:
void Update()
{
healthSlider.value = playerHealth.GetHealth(); // 체력 슬라이더 업데이트
scoreText.text = scoreKeeper.GetScore().ToString("00000"); // 점수 텍스트 업데이트
}
개념:
Update: 매 프레임 호출되는 Unity 메소드로, 실시간 데이터 갱신에 적합.
healthSlider.value: 슬라이더의 현재 값을 플레이어 체력으로 설정.
ToString("00000"): 점수를 5자리 숫자로 포맷팅(예: 00042). 이는 아케이드 스타일의 고정된 자릿수 표시를 위해 사용.
#3.6. Unity에서 연결
1. Inspector에서 연결:
캔버스 오브젝트의 UIDisplay 컴포넌트에 다음을 드래그 앤 드롭:
HealthSlider: 캔버스 내의 슬라이더 오브젝트.
PlayerHealth: 플레이어 오브젝트의 Health 컴포넌트.
ScoreText: 캔버스 내의 TextMeshPro 텍스트 오브젝트.
텍스트 오브젝트의 이름을 Text (TMP)에서 ScoreText로 변경해 가독성 향상.
2. 테스트:
게임을 실행(Play)하여 체력 슬라이더와 점수 텍스트가 실시간으로 업데이트되는지 확인.
적을 처치하면 점수가 올라가고, 체력이 감소하면 슬라이더가 반영됨.
#3.7. 점수 텍스트 포맷 개선
초기 점수 표시(예: 42)에 빈칸이 많아 보기 좋지 않음.
ToString("00000")을 사용해 점수를 5자리 고정 포맷(예: 00042)으로 변경.
결과: 점수 텍스트가 아케이드 스타일 LCD처럼 보임.
개념: ToString의 포맷 문자열(예: "00000")은 숫자를 고정 자릿수로 표시. 0은 빈 자리를 0으로 채움. 원한다면 "D5"처럼 다른 포맷도 사용 가능.
#ToString이란?
정의: 모든 C# 객체는 기본적으로 ToString 메서드를 가지고 있으며, 이를 호출하면 객체를 문자열 형태로 표현합니다.
용도: 숫자(int, float 등)를 UI 텍스트에 표시하거나, 로그 출력, 데이터 포맷팅 등에 사용.
기본 동작: 파라미터 없이 호출하면 객체의 기본 문자열 표현을 반환. 예를 들어, int 값은 숫자를 그대로 문자열로 변환.
인스펙터에 다 이어서 넣어준다.
특이한 점은 Health 스크립트는 플레이어를 끌어와서 넣어줘야 한다 ( 당연한거긴 한데 )
#Unity의 Inspector는 컴포넌트 자체를 직접 드래그할 수 없다
playerHealth는 Health 타입의 컴포넌트를 참조합니다.
Unity의 Inspector는 컴포넌트 자체를 직접 드래그할 수 없고, 그 컴포넌트가 붙어 있는 게임 오브젝트를 드래그해야 합니다.
이유: Unity는 컴포넌트가 어떤 오브젝트에 붙어 있는지 알아야 그 컴포넌트의 데이터를 참조할 수 있음.
예시:
Player 오브젝트에 Health 컴포넌트가 붙어 있다고 가정.
Inspector에서 playerHealth 필드에 Player 오브젝트를 드래그하면, Unity가 자동으로 그 오브젝트의 Health 컴포넌트를 찾아 연결.
만약 Health 스크립트 파일(프로젝트 창의 C# 파일)을 드래그하려고 하면,
Unity는 이를 인식하지 못함. 왜냐하면 스크립트 파일은 코드 템플릿일 뿐, 실제 데이터를 가진 컴포넌트가 아님.
#별도의 Scene ( 메인메뉴, 게임오버 ) 생성
#강의 흐름 요약
목표: 게임 시작 시 메인 메뉴와 게임 종료 시 게임 오버 화면을 구현.
주요 작업:
메인 메뉴와 게임 오버 화면을 각각 별도의 Scene으로 생성.
UI 요소(버튼, 텍스트 등)를 설계하고, 프리팹(재사용 가능한 오브젝트)을 활용.
점수 표시 및 버튼 동작(시작, 종료, 재시작, 메인 메뉴로 이동) 구현 준비.
다음 강의: Scene 간 전환 및 버튼 기능 연결.
#단계별 작업 정리
#1. 프로젝트 준비
Scene 이름 변경:
기본 Scene 이름(SampleScene)을 Game으로 변경해 명확히 함.
Scene 이름은 프로젝트 구조를 이해하기 쉽게 설정하는 것이 중요.
개념: Scene은 유니티에서 게임의 각 "화면" 또는 "레벨"을 나타내는 단위. 이름을 명확히 하면 팀 작업 및 유지보수에 유리.
재사용 가능한 프리팹 확인:
Background: 배경 오브젝트를 메인 메뉴와 게임 오버 화면에서 재사용.
AudioPlayer: 배경 음악 재생용 오브젝트, 모든 Scene에서 공통 사용.
ScoreKeeper: 점수를 관리하는 오브젝트. 아직 점수 표시 기능은 미완성이나, 프리팹으로 저장해 재사용 가능.
개념: 프리팹(Prefab)은 재사용 가능한 게임 오브젝트 템플릿. Scene 간 일관성을 유지하고 작업 효율성을 높임.
작업:
Background와 AudioPlayer를 [Prefabs] 폴더로 드래그해 프리팹화.
ScoreKeeper도 프리팹으로 저장, 점수 표시 기능은 나중에 구현.
#**2. 메인 메뉴 Scene 생성**
새 Scene 생성:
[Scenes] 폴더에서 우클릭 > Create > Scene > 이름: MainMenu.
저장 후 MainMenu Scene 열기.
기본 설정:
빈 Scene에 Background와 AudioPlayer 프리팹 추가.
AudioPlayer의 음악은 UI 작업 중 방해가 될 수 있으니 일시적으로 비활성화.
개념: Scene에 추가하는 오브젝트는 Hierarchy에 나타나며, 프리팹을 사용하면 동일한 설정을 여러 Scene에서 쉽게 적용 가능.
Canvas 추가:
Hierarchy에서 우클릭 > UI > Canvas 추가.
Canvas 추가 시 EventSystem이 자동 생성됨.
EventSystem의 Standalone Input Module을 새로운 입력 시스템에 맞게 교체(Replace).
개념: Canvas는 UI 요소를 배치하는 컨테이너. EventSystem은 버튼 클릭 등 사용자 입력을 처리.
Canvas 설정:
Canvas Scaler를 Scale With Screen Size로 설정, Reference Resolution 설정(예: 1920x1080).
개념: Canvas Scaler는 다양한 화면 크기에 UI를 적응시키는 도구.
Reference Resolution은 기준 해상도를 설정해 UI 크기를 일정하게 유지.
#3. 메인 메뉴 UI 설계
버튼 추가:
Canvas에서 우클릭 > UI > Button 추가.
버튼의 Transition을 Color Tint로 설정.
개념: Color Tint는 버튼의 상태(기본, 마우스 오버, 클릭 등)에 따라 색상을 변경해 사용자 피드백을 제공.
버튼 색상 설정:
Normal Color: 기본 색상(예: 민트빛 초록).
Highlighted Color: 마우스 오버 시 색상(예: 밝은 오렌지).
Pressed Color: 클릭 시 색상(예: 생동감 있는 파란색).
Selected Color: 선택 시 색상(예: 보라색).
Disabled Color: 비활성화 시 색상(예: 반투명 회색).
개념: 색상 피드백은 플레이어가 버튼 상태를 직관적으로 이해하도록 돕는 중요한 UI 요소.
텍스트 설정:
제목과 자막을 위해 Text (TMP) 추가 (TextMeshPro 사용).
새로운 Font Asset 생성 (예: KenneySpace 폰트 사용).
제목: Horizontal Gradient로 투톤 보라색, Underlay로 3D 효과, Glow로 빛 효과.
자막: main-kenney 폰트 재사용.
개념: TextMeshPro는 고품질 텍스트 렌더링 도구. Font Asset은 폰트 스타일을 커스터마이징해 UI의 시각적 품질을 높임.
버튼 그룹화:
빈 GameObject 생성, 이름: Button Group.
Vertical Layout Group 컴포넌트 추가, Spacing을 25로 설정, Control Child Size 활성화.
StartButton과 QuitButton을 추가.
개념: Layout Group은 UI 요소를 자동으로 정렬해 수동 조정 작업을 줄임.
배경 최적화:
Background의 Sprite Scroller 속도를 낮춰 메인 메뉴에서 혼란스럽지 않도록 조정.
#4. 게임 오버 Scene 생성
Scene 복제:
MainMenu Scene을 복제(Ctrl+D) > 이름: GameOver.
개념: Scene 복제는 유사한 구조를 가진 Scene을 빠르게 생성하는 방법.
UI 수정:
TitleText를 "Game Over"로 변경.
SubtitleText를 "Better Luck Next Time"으로 변경.
점수 표시를 위해 Score Text 추가 (main-kenney 폰트 사용).
버튼: Play Again과 Main Menu 버튼 추가.
개념: 게임 오버 화면은 메인 메뉴와 유사하지만, 점수 표시와 버튼 기능이 다름.
배경 및 버튼:
Background의 별 이동 속도는 메인 메뉴와 동일하게 느리게 유지.
버튼 색상은 메인 메뉴와 동일한 스타일(마우스 오버: 보라색, 클릭: 파란색).
새로 캔버스에 추가하면 이벤트 시스템에 리플레이스 눌러주기
다양한 버튼 색상 옵션
#{ 레벨 매니저(LevelManager)\*\* 여러 씬 전환 }
#1. 빌드 설정(Build Settings)
1. 유니티 빌드 설정 열기:
메뉴: File → Build Settings
목적: 게임에 포함될 씬 목록을 확인하고 관리.
2. 씬 목록 확인:
Scenes In Build에 기본 씬(예: SampleScene)이 표시됨.
이 강의에서는 기본 씬 이름을 Game으로 변경.
3. 씬 추가 및 순서 조정:
추가 씬(MainMenu, GameOver)을 Scenes In Build에 드래그하여 추가.
유니티는 빌드 인덱스(Build Index)를 기준으로 씬을 로드하며, 인덱스 0이 게임 시작 시 첫 번째로 로드됨.
MainMenu를 인덱스 0으로 설정하기 위해 드래그로 순서를 조정.
4. 설정 저장:
설정 후 Build Settings 창을 닫음(빌드 버튼 누를 필요 없음).
개념: 빌드 인덱스와 씬 관리
빌드 인덱스: 유니티에서 씬을 식별하는 숫자(0부터 시작). 게임 실행 시 첫 번째 씬(인덱스 0)부터 로드.
씬 목록 관리: 모든 씬은 Build Settings에 추가되어야 게임에서 사용 가능.
순서가 중요하며, 메인 메뉴가 첫 화면이 되도록 설정.
#2. 레벨 매니저(LevelManager) 생성
1. 게임 오브젝트 생성:
Hierarchy에서 Create Empty로 빈 게임 오브젝트를 만들고 이름은 LevelManager로 지정.
Transform Reset: 오브젝트의 위치, 회전, 크기를 초기화.
2. 스크립트 생성:
[Scripts] 폴더에서 우클릭 → Create → C# Script → 이름: LevelManager.
스크립트를 LevelManager 오브젝트에 드래그하여 컴포넌트로 추가.
3. 스크립트 작성:
네임스페이스 추가: using UnityEngine.SceneManagement;를 추가하여 씬 관리 기능 사용.
메소드 작성:
LoadGame(): 게임 씬을 로드
public void LoadGame()
{
SceneManager.LoadScene("Game"); // 씬 이름으로 로드
}
public void LoadMainMenu()
{
SceneManager.LoadScene("MainMenu");
}
public void LoadGameOver()
{
SceneManager.LoadScene("GameOver");
}
public void QuitGame()
{
Debug.Log("Quit Game");
Application.Quit(); // 게임 종료
}
#개념: SceneManager와 Application.Quit
주의: Application.Quit()은 WebGL 빌드에서 작동하지 않으며, 모바일에서는 추가 설정 필요.
개념: SceneManager와 Application.Quit
SceneManager: 유니티에서 씬을 로드하거나 관리하는 클래스. LoadScene 메소드는 씬 이름(문자열) 또는 빌드 인덱스(숫자)로 씬을 로드.
Application.Quit: 게임을 종료하는 명령. 단, WebGL이나 모바일 환경에서는 제한이 있음.
#3. 레벨 매니저를 프리팹(Prefab)으로 만들기
1. 프리팹 생성:
LevelManager 오브젝트를 [Prefabs] 폴더로 드래그하여 프리팹으로 저장.
왜 프리팹?: 모든 씬에서 동일한 LevelManager를 재사용하기 위함.
2. 버튼 연결:
MainMenu 씬:
StartButton의 On Click 이벤트에 LevelManager를 추가하고 LoadGame() 메소드 연결.
QuitButton의 On Click 이벤트에 LevelManager를 추가하고 QuitGame() 메소드 연결.
테스트: Play 버튼으로 게임 시작 및 종료 확인.
개념: 프리팹과 UI 이벤트
프리팹: 재사용 가능한 게임 오브젝트 템플릿. 모든 씬에서 동일한 설정을 유지.
On Click 이벤트: 유니티 UI 버튼에서 특정 동작(예: 메소드 호출)을 트리거하는 기능.
#4. Health 스크립트와 LevelManager 연동
#Health 스크립트 수정
LevelManager 참조 추가:
LevelManager levelManager;
void Awake()
{
levelManager = FindObjectOfType<LevelManager>();
}
Die() 메소드에서 플레이어 사망 시 LoadGameOver() 호출
void Die()
{
if (!isPlayer)
{
scoreKeeper.ModifyScore(score);
}
else
{
levelManager.LoadGameOver();
Debug.Log("Player has died");
}
Destroy(gameObject);
}
#Game 씬에 LevelManager 추가:
LevelManager 프리팹을 Game 씬에 추가.
개념: FindObjectOfType
FindObjectOfType: 씬에서 특정 컴포넌트(예: LevelManager)를 찾아 참조. 성능상 주의 필요(빈번한 사용은 피해야 함).
#5. GameOver 씬 버튼 연결
1. GameOver 씬 설정:
LevelManager 프리팹을 GameOver 씬에 추가.
ReplayButton의 On Click 이벤트에 LevelManager → LoadGame() 연결.
MainMenuButton의 On Click 이벤트에 LevelManager → LoadMainMenu() 연결.
2. 테스트:
게임 플레이 → 사망 → GameOver 씬 전환 → 버튼으로 재시작 또는 메인 메뉴 이동 확인.
개념: 씬 간 전환
버튼과 LevelManager를 통해 씬 간 부드러운 전환 구현. 각 버튼은 특정 씬을 로드하도록 설정.
#6. 게임 오버 지연 로딩
코루틴 추가:
LevelManager에 지연 로딩을 위한 코루틴 작성
[SerializeField] float sceneDelay = 2f;
IEnumerator WaitAndLoad(string sceneName, float delay)
{
yield return new WaitForSeconds(delay);
SceneManager.LoadScene(sceneName);
}
LoadGameOver() 수정:
public void LoadGameOver()
{
StartCoroutine(WaitAndLoad("GameOver", sceneDelay));
}
2. SerializeField 사용:
sceneDelay를 인스펙터에서 조정 가능하도록 설정.
기본값: 2초.
3. 프리팹 업데이트:
LevelManager 프리팹에서 sceneDelay 값을 조정.
4. 테스트:
플레이어 사망 후 2초 지연 후 GameOver 씬으로 전환 확인.
개념: 코루틴과 SerializeField
코루틴: 유니티에서 비동기 작업(예: 지연)을 처리하는 강력한 도구. IEnumerator와 yield return 사용.
SerializeField: private 변수를 유니티 인스펙터에 노출하여 값을 쉽게 조정 가능.
빌드 셋팅에 새로 만든 씬 추가
LevelManager 오브젝트 ( 스크립트 말고 ) 를 버튼에 이어주고 버튼에 함수를 연결되게 해준다 ( ex. LoadGame )
#{ 싱글턴 패턴을 사용한 AudioPlayer 구현 }
이 강의에서는 Unity 게임에서 AudioPlayer가 씬(Scene) 전환 시 음악이 끊기는 문제를 해결하기 위해
**싱글턴 패턴(Singleton Pattern)**을 적용하는 방법을 배웁니다.
#1. 문제 상황
문제: 메인 메뉴에서 게임 씬으로 전환할 때, 기존 씬의 오브젝트(예: AudioPlayer)가 파괴되고
새 AudioPlayer가 생성되면서 음악이 처음부터 재생됨.
해결책: AudioPlayer를 씬 전환 시에도 유지되도록 싱글턴 패턴을 사용해 단일 인스턴스로 관리.
#2. 싱글턴 패턴이란?
정의: 클래스의 인스턴스를 단 하나로 제한하는 소프트웨어 디자인 패턴.
특징:
특정 클래스의 객체가 게임 전체에서 하나만 존재하도록 보장.
주로 글로벌 접근이 필요한 경우 사용 (예: 오디오 관리, 게임 매니저).
주의점:
싱글턴은 안티 패턴으로 간주될 수 있음.
과도한 글로벌 변수 사용은 코드 복잡성을 증가시키고 디버깅을 어렵게 만듦.
Unity에서는 신중히 사용해야 함.
#3. 싱글턴 패턴 구현 방법
강의에서는 AudioPlayer를 싱글턴으로 만드는 두 가지 방법을 소개합니다.
#방법 1: 비공개 싱글턴 (Private Singleton)
목표: AudioPlayer 인스턴스를 하나만 유지하고, 다른 스크립트에서 직접 접근하지 못하도록 제한.
구현:
Awake에서 싱글턴 관리:
ManageSingleton 메서드를 호출해 인스턴스 수를 체크.
FindObjectsOfType<AudioPlayer>().Length로 현재 씬의 AudioPlayer 수 확인.
인스턴스가 1개 초과면 현재 게임 오브젝트를 비활성화(gameObject.SetActive(false)) 후 파괴(Destroy(gameObject)).
인스턴스가 하나뿐이면 DontDestroyOnLoad(gameObject)를 호출해 씬 전환 시에도 유지.
void Awake()
{
ManageSingleton();
}
void ManageSingleton()
{
int instanceCount = FindObjectsOfType<AudioPlayer>().Length;
if (instanceCount > 1)
{
gameObject.SetActive(false);
Destroy(gameObject);
}
else
{
instance = this;
DontDestroyOnLoad(gameObject);
}
}
비활성화 이유:
오브젝트가 파괴되기 전에 다른 스크립트가 접근할 가능성을 차단.
Unity의 실행 순서로 인해 드물게 발생할 수 있는 문제를 방지.
장점: 간단하고, AudioPlayer가 내부적으로만 관리되므로 외부 접근을 제한해 안전.
단점: 다른 스크립트에서 AudioPlayer를 참조하려면 명시적으로 찾아야 함.
#방법 2: 공개 싱글턴 (Public Singleton)
목표: AudioPlayer 인스턴스를 하나로 유지하면서 다른 스크립트에서 쉽게 접근 가능하도록 설정.
구현:
1. 정적 변수 추가:
static AudioPlayer instance를 클래스 상단에 선언.
static 키워드는 클래스의 모든 인스턴스가 공유하는 단일 변수를 만듦.
2. ManageSingleton 수정:
FindObjectsOfType 대신 instance 변수를 체크.
첫 번째 AudioPlayer면 instance = this로 설정하고 DontDestroyOnLoad 호출.
이후 생성된 AudioPlayer는 비활성화 후 파괴.
3. 공개 접근 제공:
public static AudioPlayer GetInstance() { return instance; } 메서드를 추가해 다른 스크립트에서 인스턴스에 접근 가능.
static AudioPlayer instance;
void ManageSingleton()
{
if (instance != null)
{
gameObject.SetActive(false);
Destroy(gameObject);
}
else
{
instance = this;
DontDestroyOnLoad(gameObject);
}
}
public static AudioPlayer GetInstance()
{
return instance;
}
사용 예시:
다른 스크립트에서 AudioPlayer.GetInstance().PlayShootingClip()처럼 호출 가능.
이는 FindObjectOfType을 호출해 오브젝트를 찾는 과정을 생략.
장점:
글로벌 접근이 쉬워 코드가 간결해짐.
AudioPlayer를 씬에서 직접 찾을 필요 없음.
단점:
공개 접근은 다른 스크립트에서 AudioPlayer를 무분별하게 사용할 가능성을 높임.
대규모 프로젝트에서는 의존성 관리가 복잡해질 수 있음.
#4. Unity에서의 실행 결과
비공개 싱글턴:
메인 메뉴에서 게임 씬으로 전환 시 AudioPlayer가 DontDestroyOnLoad에 추가되어 유지됨.
음악이 끊기지 않고 연속 재생.
Hierarchy에서 AudioPlayer가 DontDestroyOnLoad 아래에 표시됨.
공개 싱글턴:
다른 스크립트에서 AudioPlayer.GetInstance()로 쉽게 접근 가능.
하지만 글로벌 접근으로 인해 프로젝트가 커질수록 관리 어려움.
#5. 싱글턴 패턴의 주의점
장점:
오브젝트가 하나만 존재하도록 보장.
씬 전환 시 오브젝트 유지 가능.
단점:
과도한 사용은 코드의 **결합도(Coupling)**를 높이고, 디버깅과 유지보수를 어렵게 만듦.
공개 싱글턴은 다른 스크립트의 무분별한 접근으로 예기치 않은 문제를 일으킬 수 있음.
권장사항:
AudioPlayer처럼 단순한 경우에 적합.
대규모 프로젝트에서는 의존성 주입(Dependency Injection) 같은 대안을 고려.
#{ 싱글턴 패턴과 ScoreKeeper 개선 }
#1. 강의 목표
ScoreKeeper를 싱글턴(Singleton) 패턴으로 전환하여 여러 씬(Scene) 간 점수 데이터를 유지하고 관리.
UIGameOver 스크립트를 작성해 게임 오버 화면에서 점수를 표시.
LevelManager에서 점수 초기화를 적절히 처리하여 게임 루프 완성.
#2. 주요 개념: 싱글턴 패턴
싱글턴이란?
싱글턴은 특정 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 디자인 패턴입니다.
유니티에서는 씬 전환 시 객체가 유지되도록 DontDestroyOnLoad를 사용해 구현.
예: AudioPlayer를 싱글턴으로 만들어 씬이 바뀌어도 음악이 끊기지 않게 함.
장점: 전역 접근 가능, 중복 객체 방지.
단점: 과도한 사용 시 코드 복잡성 증가, 의존성 문제 발생 가능.
싱글턴의 적합성 판단
모든 객체가 싱글턴으로 적합하지 않음. 예: LevelManager는 여러 씬에 존재해도 큰 문제를 일으키지 않으므로 싱글턴으로 만들 필요가 적음.
ScoreKeeper는 점수 데이터가 모든 씬에서 일관되게 유지되어야 하므로 싱글턴에 적합.
#3. ScoreKeeper 싱글턴 구현
public class ScoreKeeper : MonoBehaviour
{
int score = 0;
static ScoreKeeper instance;
void Awake()
{
ManageSingleton();
}
void ManageSingleton()
{
if (instance != null)
{
gameObject.SetActive(false);
Destroy(gameObject);
}
else
{
instance = this;
DontDestroyOnLoad(gameObject);
}
}
public int GetScore() { return score; }
public void ModifyScore(int value)
{
score += value;
score = Mathf.Clamp(score, 0, int.MaxValue);
Debug.Log("Score updated: " + score);
}
public void ResetScore() { score = 0; }
}
static ScoreKeeper instance: 클래스의 단일 인스턴스를 저장.
ManageSingleton: 새로운 ScoreKeeper가 생성될 때 기존 인스턴스가 있으면 새 객체를 파괴하고,
없으면 현재 객체를 유지하며 DontDestroyOnLoad로 씬 전환 시 파괴되지 않도록 설정.
GetScore, ModifyScore, ResetScore: 점수 관리 메서드.
#4. UIGameOver 스크립트 작성
목적: 게임 오버 화면에서 점수를 표시.
public class UIGameOver : MonoBehaviour
{
[SerializeField] TextMeshProUGUI scoreText;
ScoreKeeper scoreKeeper;
void Awake()
{
scoreKeeper = FindAnyObjectByType<ScoreKeeper>();
}
void Start()
{
scoreText.text = "You Scored:\n" + scoreKeeper.GetScore();
}
}
설명
TextMeshProUGUI: UI 텍스트를 표시하기 위한 컴포넌트.
Awake: ScoreKeeper를 찾아 참조 저장.
Start: 점수를 텍스트로 표시. \n은 줄바꿈을 위해 사용.
문자열 연결: GetScore()는 정수를 반환하지만, 문자열과 연결 시 자동으로 문자열로 변환됨.
#5. LevelManager 수정
문제점: 게임 재시작 시 점수가 초기화되지 않음.
해결: LoadGame에서 ResetScore 호출.
public class LevelManager : MonoBehaviour
{
[SerializeField] float sceneDelay = 2f;
ScoreKeeper scoreKeeper;
void Awake()
{
scoreKeeper = FindObjectOfType<ScoreKeeper>();
}
public void LoadGame()
{
scoreKeeper.ResetScore();
SceneManager.LoadScene("Game");
AudioPlayer audioPlayer = FindObjectOfType<AudioPlayer>();
if (audioPlayer != null)
{
audioPlayer.PlayGameMusic();
}
}
}
설명
Awake: ScoreKeeper를 찾아 참조 저장.
LoadGame: 씬 로드 전에 ResetScore를 호출해 점수 초기화.
개념: FindObjectOfType은 씬에서 컴포넌트를 찾지만,
싱글턴 패턴을 사용하면 전역 접근이 가능해 코드가 간소화될 수 있음. 하지만 여기서는 명시적 참조를 유지.
#6. 왜 LevelManager는 싱글턴으로 만들지 않았나?
이유
여러 LevelManager가 존재해도 게임 로직에 큰 영향을 미치지 않음.
버튼 등 UI 요소가 특정 씬의 LevelManager를 참조하므로, 싱글턴으로 만들면 참조 문제가 발생할 수 있음.
프리팹 설정으로 sceneDelay 등을 일관되게 관리 가능.
결론: LevelManager는 현재 구조로 충분히 작동하므로 싱글턴 전환 불필요.
#7. 추가 팁
싱글턴 사용 시 주의점
싱글턴은 편리하지만, 과도하게 사용하면 코드 의존성이 높아져 유지보수가 어려워질 수 있음.
대안으로 이벤트 시스템이나 매니저 클래스를 고려할 수 있음.
작동 흐름
1. 게임 시작 → ScoreKeeper 싱글턴 인스턴스 생성, 점수 초기화.
2. 게임 플레이 → ModifyScore로 점수 증가.
3. 게임 오버 → UIGameOver가 ScoreKeeper에서 점수를 가져와 표시.
4. 재시작 → LevelManager가 ResetScore 호출 후 새 게임 씬 로드.
캔버스에 UI GameOver 스크립트 추가 캔버스의 스코어 텍스트 넣어주기
#{ 버그 수정, 밸런싱, 플레이 테스트 및 빌드 }
#1. 플레이 테스트 및 밸런싱
모든 씬을 점검하고, 게임의 재미와 난이도를 조정하는 단계.
2.1. MainMenu 씬 점검
상태: 기본적으로 잘 작동.
문제점:
Quit 버튼: WebGL 플랫폼에서는 Quit 기능이 작동하지 않으므로, WebGL 빌드 시 제거 필요.
크레딧 메뉴 부족: 시작 화면에 크레딧 버튼을 추가해 자산 제공자와 프로젝트 기여자에게 감사를 표할 수 있음.
개선 방법:
Button Group 아래에 크레딧 버튼 추가.
새 캔버스를 만들어 기존 캔버스 위에 오버레이로 크레딧 화면 구성.
개념: 플랫폼별 호환성을 고려해야 함. WebGL은 브라우저 기반이므로 Quit 같은 네이티브 기능이 제한됨.
UI 설계 시 캔버스 오버레이는 메뉴 전환을 부드럽게 처리하는 방법.
2.2. GameOver 씬 점검
상태: 기본 기능은 정상 작동.
고려 사항: Canvas Scaler 설정.
문제: Reference Resolution이 9:16 비율로 설정되어 있지만, 플레이어가 16:9 화면에서 플레이하면 UI(예: MainMenu 버튼)가 잘릴 수 있음.
해결 옵션:
높이에 맞춤: 화면 양옆을 확장해 모든 UI가 보이도록 설정.
프로젝트 설정 변경: 빌드 설정에서 해상도 조정.
권장: 빌드 전 Canvas Scaler 설정을 점검해 UI가 다양한 화면 비율에서 잘 보이도록 조정.
개념: Canvas Scaler는 UI 요소의 크기와 위치를 다양한 해상도에 맞게 조정하는 Unity의 도구.
Reference Resolution과 Scale With Screen Size 설정을 통해 UI 호환성을 보장.
2.3. Game 씬 점검
플레이어:
설정 조정:
Player (Script)에서 이동 속도와 화면 제약(top 패딩값) 조정, 플레이어가 화면 하단 반 정도에서만 움직이도록 제한.
Circle Collider의 Offset과 Radius를 조정해 충돌 감지 개선.
Health와 Shooter는 현재 적절한 상태로 유지.
개념: 밸런싱은 플레이어의 조작감과 난이도를 조정하는 과정. Collider는 물리적 충돌을 계산하므로,
크기와 위치를 조정해 게임의 자연스러운 느낌을 개선.
적군:
새로운 적군 추가: 기존 적군 복제 후 에너지와 점수 조정(Enemy 1은 낮은 점수와 에너지).
Shooter (Script)에서 발사 패턴 파라미터 수정.
개념: 적군의 난이도와 보상(점수)은 게임의 재미와 도전 요소를 결정. 파라미터 튜닝은 플레이 테스트를 통해 최적화.
Waves와 Paths:
다양한 패턴의 Wave와 Path 추가.
EnemySpawner에 새로운 Wave 추가로 다양성 확보.
개념: Wave 시스템은 적군의 출현 패턴을 관리. 다양한 패턴은 게임의 반복성을 줄이고 플레이어의 흥미를 유지.
AudioPlayer:
음악과 효과음 볼륨 조정(과도하지 않도록).
발사음 빈도가 높을 경우, 귀에 거슬리지 않도록 조정.
개념: 오디오 밸런싱은 게임의 몰입감을 높이고, 과도한 소음으로 인한 피로를 방지.
#2. 게임 빌드 및 배포
#2.1. 빌드 설정
경로: File > Build Settings.
씬 추가: 빌드에 포함할 씬을 추가.
플랫폼 선택: PC, Mac, Linux, WebGL 등. 초기 설치에 따라 선택 가능한 플랫폼이 달라짐.
개념: Build Settings는 게임을 실행 가능한 파일로 변환하는 설정 창. 플랫폼별 호환성을 고려해 설정.
#2.2. 플레이어 설정 (Player Settings)
Resolution and Presentation:
Fullscreen Mode: 전체 화면 또는 창 모드 선택.
Default Screen Width/Height: 특정 해상도 강제 설정 가능.
WebGL 빌드 시 기본 캔버스 해상도 조정(예: 9:16 비율로 540x960).
Compression Format: Disabled로 설정해 공유 플랫폼 호환성 확보.
개념: Player Settings는 빌드된 게임의 실행 환경을 정의. 해상도 설정은 다양한 디바이스에서 일관된 경험을 제공.
#2.3. 빌드 과정
폴더 생성: [Builds] 폴더와 플랫폼별 하위 폴더(예: [Standalone], [WebGL]) 생성.
빌드 실행: Build 버튼 클릭 후 대상 폴더 선택.
소요 시간: 프로젝트 크기에 따라 몇 초에서 몇 시간 소요.
개념: 빌드는 게임을 실행 가능한 형태로 변환. WebGL은 브라우저 실행을 위해 압축 및 최적화 필요.
#2.4. 게임 배포
플랫폼: Sharemygame.com에서 게임 업로드.
업로드 과정:
1. 계정 로그인/회원가입.
2. Upload a Game 선택 후 WebGL 폴더 업로드.
3. 업로드 완료 후 게임 테스트 및 피드백 확인.
4. 수정 및 재업로드: 문제가 있으면 프로젝트 수정 후 새 버전 업로드.
개념: 게임 배포는 커뮤니티와 게임을 공유하는 단계. 피드백을 받아 개선점을 도출.
화면의 높이에 맞추면 모든 화면의
양옆을 확장 옵션
패딩을 수정해서 플레이어 캐릭터 이동 범위 화면의 반 정도로 제약
빌드 셋팅
빌드에서 플레이어 셋팅으로 들어가서 화면 크기를 고정하는 옵션을 선택
( with, height 를 반대로 적어놨는데 9:16비율로 게임을 만들었으니 거기에 맞춰서 값을 잘 넣어줘야한다 )
빌드 진행 -> 빌드 완료
WebGL 로 전환
Resolution... 에서 해상도 조절
이후 Compression Format 을 Disabled 로 변경
#Netlify에 Unity WebGL 프로젝트를 배포
#1단계: Unity WebGL 빌드 준비
Unity에서 빌드하기
1. Unity에서 File > Build Settings 선택
2. Platform을 WebGL로 변경
3. Player Settings에서 다음 설정 확인:
Publishing Settings > Compression Format: Gzip 또는 Brotli
Resolution and Presentation > WebGL Template: Default 또는 Minimal
4. Build 버튼 클릭하여 빌드 폴더 생성
빌드 폴더 구조 확인
빌드 완료 후 다음과 같은 파일들이 생성됩니다:
MyGame/
├── Build/
│ ├── MyGame.data
│ ├── MyGame.framework.js
│ ├── MyGame.loader.js
│ └── MyGame.wasm
├── TemplateData/
│ └── (CSS, 이미지 파일들)
└── index.html
#2단계: Netlify 계정 생성 및 설정
1. Netlify 웹사이트 접속
https://netlify.com 방문
Sign up 클릭하여 계정 생성 (GitHub/GitLab/Email 선택 가능)
2. 대시보드 접속
로그인 후 Netlify 대시보드로 이동
#3단계: 프로젝트 배포하기
방법 1: 드래그 앤 드롭 (가장 간단)
1. 파일 압축
Unity 빌드 폴더 전체를 ZIP 파일로 압축
또는 빌드 폴더 내의 모든 파일과 폴더를 선택
2. Netlify에 업로드
Netlify 대시보드에서 Add new site > Deploy manually 선택
또는 페이지 하단의 "Want to deploy a new site without connecting to Git? Drag and drop your site output folder here" 영역 사용
ZIP 파일을 드래그해서 해당 영역에 드롭
또는 폴더를 직접 드래그 앤 드롭
3. 배포 대기
파일 업로드 및 배포 과정이 자동으로 진행됩니다
보통 1-2분 정도 소요
#UIGameOver에서 ScoreKeeper를 찾는 시점을 Awake 대신 Start로 옮겨 문제 해결
스코어 키퍼에 문제가 생겨 스코어가 Awake 에서 초기화 되지 못해 UIGameOver 에서 0 으로 나오는 버그가 있었다
Awake 에서 start 로 초기화 코드를 옮겨주니 해결되었다 이는 WebGL 의 문제였다
UIGameOver가 Awake에서 ScoreKeeper를 찾을 때, WebGL의 느린 씬 로드 타이밍 때문에
ScoreKeeper가 아직 초기화되지 않아 null이 반환되거나 스코어가 0으로 표시됨.
#해결 방법 및 이유
방법 1: Awake → Start로 변경
왜? Start는 모든 오브젝트가 초기화된 후 호출되므로 ScoreKeeper를 더 안정적으로 찾음.
효과: WebGL의 불규칙한 로드 타이밍 문제를 피함.
방법 2: 이벤트 기반 접근법
왜? ScoreKeeper의 스코어 변경 시 이벤트를 통해 UIGameOver가 자동으로 업데이트되므로 타이밍에 의존하지 않음.
효과: 스코어가 실시간으로 반영되어 안정적.